/**
 * @description 元素挂载、更新操作
 */

import { patch } from "./render.js";
import {
  createElement,
  setElementText,
  unmount,
  insert,
} from "../utils/browserDOMAPI.js";
import { getSequence } from "./get-sequence.js";

/**
 * @description 正常化 class 值为 对象时，将该对象转 为一字符串
 * @param {*} obj 对象
 * @returns
 */
function classValueObjToString(obj) {
  /* 过滤出 对象中值为 true 的属性 */
  return Object.keys(obj)
    .filter((key) => {
      return obj[key];
    })
    .join(" ");
}

/**
 * @description 正常化 class 值为 数组时，将该对象转 为一字符串
 * @param {*} obj 对象
 * @returns
 */
function classValueArrToString(arr) {
  /* 遍历数据中的元素，如果为对象那么 对其进行 对象转字符串的处理 */
  return arr
    .map((item) => {
      if (typeof item === "object") {
        item = classValueObjToString(item);
      }

      return item;
    })
    .join(" ");
}

/**
 * @description 正常化 class 值 为一字符串
 * @param {*} value class 值
 * @returns { String }
 */
function normalizeClass(value) {
  if (typeof value === "object") {
    value = Array.isArray(value)
      ? classValueArrToString(value)
      : classValueObjToString(value);
  }

  console.log(value);

  return value;
}

/**
 * @description 对只读的DOM Properties 做特殊处理
 * @param {*} el 元素
 * @param {*} key 属性名
 * @param {*} value 属性值
 */
function shouldSetAsProps(el, key, value) {
  if (key === "form" && el.tagName === "INPUT") {
    return false;
  }

  return key in el;
}

/**
 * @description 设置元素的属性以及属性值
 * @param {*} el 元素
 * @param {*} key 属性名
 * @param {*} preValue 旧值
 * @param {*} nextValue 新值
 */
export function patchProps(el, key, preValue, nextValue) {
  /* 处理事件属性 */
  if (/^on/.test(key)) {
    /* 元素身上的所有事件与事件处理函数的映射表 */
    const invokers = el._vei || (el._vei = {});
    /* 当前读取的事件的事件处理函数/事件处理函数集 */
    let invoker = invokers[key];
    /* 事件名称 */
    const name = key.slice(2).toLowerCase();
    /* 如果新的虚拟节点上的事件处理函数存在 */
    if (nextValue) {
      /* invoker 伪事件处理函数不存在, 表示第一次绑定该事件  */
      if (!invoker) {
        /* 创建 invoker 并将 伪事件处理函数存储到 el._vei 属性中 */
        invoker = el._vei[key] = (e) => {
          /* 屏蔽事件所有触发时间早于绑定时间的处理函数执行
               解决事件冒泡带来的问题
               e.timeStamp： 事件触发时间
               invoker.attached：事件绑定时间
            */
          if (e.timeStamp < invoker.attached) return;

          /* 执行真正的 事件处理函数 */
          /* 如果取得的是一个数组（一个事件多个事件处理函数），遍历执行所有处理函数 */
          if (Array.isArray(invoker.value)) {
            invoker.value.forEach((fn) => fn(e));
          } else {
            /* 单个处理函数执行 */
            invoker.value(e);
          }
        };

        /* 将事件处理函数存储到 invoker 伪处理函数的 value 属性中 */
        invoker.value = nextValue;

        /* 事件绑定的时间，performance.now()：表示从页面加载开始或 performance.timeOrigin 开始的相对时间 */
        invoker.attached = performance.now();

        /* 给元素添加事件，并绑定事件处理函数 */
        el.addEventListener(name, invoker);
      } else {
        /* 如果新旧处理函数都存在，即新的处理函数替换旧的事件处理函数 */
        invoker.value = nextValue;
      }
    } else {
      /* 如果节点上新的属性没有该事件则删除 */
      el.removeEventListener(name, invoker);
    }
  } else if (key === "class") {
    /* 遇到属性是class，在赋值给className进行特殊处理，再把特殊处理过的字符串赋值给className */
    const value = normalizeClass(nextValue);
    el.className = value || "";
  } else if (shouldSetAsProps(el, key, nextValue)) {
    /* 如果设置的属性是form并且元素是input元素则返回false， 如果HTML元素上存在该属性返回true */
    /* 获取el的属性的类型 */
    const type = typeof el[key];

    /* 该属性（disabled）的值的类型是布尔值，并且即将要设置给该属性的值的新值为空字符串 */
    if (type === "boolean" && value === "") {
      /* 那么设置该属性的值为true，disabled=true 表示禁用表单项 */
      el[key] = true;
    } else {
      /* 否则将新值设置到该属性上（javascript DOM 对象中存在，并为可读可写的属性），有任何值都为不禁用 */
      el[key] = nextValue;
    }
  } else {
    /* 如果HTML元素本身没有这个属性，或者是javascript DOM对象中为只读的属性，那么使用setAttribute来设置自定义属性 */
    el.setAttribute(key, nextValue);
  }
}

/**
 * @description 首次挂载虚拟节点，渲染为真实Dom节点
 * @param {*} vnode
 * @param {*} container
 */
export function mountElement(vnode, container, anchor) {
  /* 创建虚拟节点对应的真实dom节点 */
  /* vnode.el 表示虚拟节点对应的真实dom节点 */
  const el = (vnode.el = createElement(vnode.type));

  /* 处理子节点，如果子节点是字符串，那么该子节点为 文本节点 */
  if (typeof vnode.children === "string") {
    setElementText(el, vnode.children);
  } else if (Array.isArray(vnode.children)) {
    /* 如果children是一个数组，则表示该节点多个子节点 */
    /* 循环遍历children，然后挂载这些子节点 */
    vnode.children.forEach((child) => {
      /* patch的第一个参数为null表示没有旧的节点，表示第一次挂载 */
      patch(null, child, el);
    });
  }

  /* props存在才给元素添加属性 */
  if (vnode.props) {
    /* 遍历虚拟节点对象的props属性，给元素设置属性 */
    for (let key in vnode.props) {
      /* 设置元素的属性以及属性值 */
      patchProps(el, key, null, vnode.props[key]);
    }
  }

  /* 需要添加过渡动画的VNode */
  const needTransition = vnode.transition;

  if (needTransition) {
    vnode.transition.beforeEnter(el);
  }

  /* 将创建的真实dom节点添加到容器中 */
  insert(el, container, anchor);

  if (needTransition) {
    vnode.transition.enter(el);
  }
}

/**
 * @description 非首次挂载节点的情况下，更新元素
 * @param {*} oldVnode  旧的虚拟节点
 * @param {*} vnode 新的虚拟节点
 */
export function patchElement(oldVnode, vnode) {
  /* 新旧虚拟节点元素一样的情况下，更新元素的属性 */
  const el = (vnode.el = oldVnode.el);

  /* 存储旧元素节点的属性 */
  const oldProps = oldVnode.props;

  /* 存储新元素节点的属性 */
  const newProps = vnode.props;

  /* 更新元素的属性 */

  /* 遍历新的元素属性对象的键 */
  for (const key in newProps) {
    /* 如果用新的元素属性对象中的键在旧的元素属性对象中获取到的相应的属性的值不一致 */
    if (oldProps[key] !== newProps[key]) {
      /* 更新在旧元素属性对象中的对应属性的值 */
      patchProps(el, key, oldProps[key], newProps[key]);
    }
  }

  /* 遍历旧的元素属性对象的键 */
  for (const key in oldProps) {
    /* 如果旧的元素属性对象的键不存在于新的属性对象，那么删除掉该属性 */
    if (!(key in nextProps)) {
      patchProps(el, key, oldProps[key], null);
    }
  }

  /* 更新子节点 */
  patchChildren(oldVnode, vnode, el);
}

/**
 * @description 对比子节点
 * @param {*} oldVnode 旧的虚拟节点
 * @param {*} vnode 新的虚拟节点
 * @param {*} container 容器
 */
export function patchChildren(oldVnode, vnode, container) {
  /* 如果新的子节点是文本 */
  if (typeof vnode.children === "string") {
    /* 如果旧的虚拟节点有子节点并且为非文本节点 */
    if (Array.isArray(oldVnode.children)) {
      /* 删除旧节点的所有元素 */
      oldVnode.children.forEach((child) => {
        unmount(child);
      });
    }
    /* 设置内容为新的文本内容 */
    setElementText(container, vnode.children);
  } else if (Array.isArray(vnode.children)) {
    /* 如果新的虚拟节点是一个数组，代表是非文本节点 */
    if (Array.isArray(oldVnode.children)) {
      /* 双端diff算法 */
      patchKeyedChildren(oldVnode, vnode, container);
    } else {
      /* 清空文本 */
      setElementText(container, "");
      /* 新挂载非文本节点 */
      vnode.children.forEach((child) => {
        patch(null, child, container);
      });
    }
  } else {
    /* 新的虚拟节点没有子节点 */

    /* 如果旧的虚拟节点有子节点并且是非文本节点 */
    if (Array.isArray(oldVnode.children)) {
      /* 那么将旧节点的所有子节点全部卸载 */
      oldVnode.children.forEach((child) => unmount(child));
    } else if (typeof oldVnode.children === "string") {
      /* 如果旧节点的子节点是文本节点 */
      /* 将旧节点的文本内容设置为空 */
      setElementText(container, "");
    }
    /* 如果旧节点没有子节点，则不需要做什么 */
  }
}

/**
 * @description 快速diff算法
 * @param {*} oldVnode 旧虚拟节点
 * @param {*} newVnode 新虚拟节点
 * @param {*} container 容器
 */
function patchKeyedChildren(oldVnode, newVnode, container) {
  /* 旧的一组Vnode子节点 */
  const oldChildren = oldVnode.children;
  /* 新的一组Vnode子节点 */
  const newChildren = newVnode.children;

  /* 新旧一组子节点头部索引 */
  let startIndex = 0;

  /* 旧的一组子节点的从头部开始获取的节点 */
  let oldStartVnode = oldChildren[startIndex];
  /* 新的一组子节点的从头部开始获取的节点 */
  let newStartVnode = newChildren[startIndex];

  /* 用while循环向后遍历，查找相同key的前置节点，直到找到不同key的子节点为止，完成头部节点的更新 */
  while (oldStartVnode.key === newStartVnode.key) {
    /* 相同的子节点不需要移动位置，只需要进行更新属性、以及后代即可 */
    patch(oldStartVnode, newStartVnode, container);
    /* 往后更新节点头部索引 */
    startIndex++;
    /* 更新新旧头部节点 */
    oldStartVnode = oldChildren[startIndex];
    newStartVnode = newChildren[startIndex];
  }

  /* 旧的一组子节点尾部索引 */
  let oldEndIndex = oldChildren.length - 1;
  /* 新的一组子节点尾部索引 */
  let newEndIndex = newChildren.length - 1;

  /* 旧的尾部子节点 */
  let oldEndVnode = oldChildren[oldEndIndex];
  /* 新的尾部子节点 */
  let newEndVnode = newChildren[newEndIndex];

  while (oldEndVnode.key === newEndVnode.key) {
    /* 更新相同新旧子节点属性、子节点... */
    patch(oldEndVnode, newEndVnode, container);
    /* 更新新旧的一组子节点尾部索引 */
    oldEndIndex--;
    newEndIndex--;

    /* 更新新旧一组子节点的尾部节点 */
    oldEndVnode = oldChildren[oldEndIndex];
    newEndVnode = newChildren[newEndIndex];
  }

  /* 新增节点 */
  /* 如果旧的一组子节点尾部索引 < 一组节点的头部索引（代表旧的一组子节点中的所有子节点都已对比更新完成） */
  /* 并且新的一组子节点尾部索引 >= 一组子节点的头部索引（代表新的一组子节点中还有未更新的子节点，这些节点就是新增的子节点） */
  if (oldEndIndex < startIndex && newEndIndex >= startIndex) {
    /* 找到新增子节点的锚点元素 */
    // 锚点元素的索引
    let anchorIndex = newEndIndex + 1;
    /* 锚点元素，如果锚点元素索引小于新的一组子节点的数量 */
    /* 说明锚点元素在一组新的子节点中，则使用newChildren[anchorIndex].el作为锚点元素 */
    /* 否则说明newEndIndex对应的子节点是新的一组子节点中的最后一个子节点，此时不需要锚点元素 */
    const anchor =
      anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null;
    /* 从头部开始遍历，直到尾部索引结束，依次将新增的子节点添加到容器中 */
    while (startIndex <= newEndIndex) {
      patch(null, newChildren[startIndex++], container, anchor);
    }
  } else if (newEndIndex < startIndex && oldEndIndex >= startIndex) {
    /* 删除节点 */
    /* 如果新的一组子节点的尾部索引 < 一组子节点的头部索引（代表新的一组子节点中的所有子节点都已对比更新完毕） */
    /* 并且旧的一组子节点的尾部索引 >= 一组子节点的头部索引（说明旧的一组子节点中还有未处理的子节点，这些子节点就是需要删除的子节点） */
    while (startIndex <= oldEndIndex) {
      unmount(oldChildren[startIndex++]);
    }
  } else {
    /** 定义 source 数组，用来存储新的一组子节点在旧的一组子界点中对应的索引，
     * source数组的长度就是在新的一组子节点中没有被处理的那些子节点，
     * 数组中默认每个元素为 -1 表示旧的一组子节点中没有该新的子节点对应的节点。
     *  */
    // 新的一组子节点中未被处理的子节点的数量
    const count = newEndINdex - startIndex + 1;
    const source = new Array(count).fill(-1);

    /* 新旧节点的头部索引，就是 startIndex */
    const oldStartIndex = startIndex;
    const newStartIndex = startIndex;
    /* 节点对应的真实DOM是否需要移动 */
    let moved = false;
    /* 遍历旧的一组子节点过程中，遇到的最大索引值（newVnodeIndex） */
    let pos = 0;

    /* 创建新的一组子节点key与index索引的映射表 */
    const keyIndexMap = new Map();

    /* 遍历新的一组子节点中未被处理的子节点，添加子节点key与index索引的映射关系 */
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      const newVnode = newChildren[i];
      keyIndexMap.set(newVnode.key, i);
    }

    /* 除新旧子节点中头尾相同的节点，已经更新过的节点 */
    let patched = 0;

    /** 遍历旧的一组子节点中未被处理的子节点，
     * 找到与新的一组子节点中相同的节点并进行更新，如果没找到，将旧的子节点的真实DOM删除
     *  */
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      const oldVnode = oldChildren[i];

      /* 更新过的子节点数量小于应该更新的子节点数量，进行更新 */
      if (patched <= count) {
        /* 通过旧子节点的key从新的一组子节点的key与index的映射表中获取旧的子节点在新的一组子节点中相同key的新子节点的index */
        const newVnodeIndex = keyIndexMap.get(oldVnode.key);
        /* 如果旧的一组子节点中的子节点能够在新的一组子节点的key与index的映射表中找到对应的index，表示新的一组子节点中有整个旧的子节点 */
        if (typeof newVnodeIndex !== "undefined") {
          /* 更新真实dom的属性、子节点... */
          patch(oldVnode, newChildren[newVnodeIndex], container);
          /* 每更新一个节点， patched++ */
          patched++;
          /* 更新source中新的一组子节点未被处理的子节点在旧的一组子节点中对应的节点的index索引 */
          source[newVnodeIndex - newStartIndex] = i;

          /* 判断是否需要移动节点对应的真实DOM */
          if (newVnodeIndex < pos) {
            moued = true;
          } else {
            pos = newVnodeIndex;
          }
        } else {
          /* 如果没有找到，表示新的一组子节点中没有该旧子节点，要将其对应的真实DOM卸载 */
          unmount(oldVnode);
        }
      } else {
        /* 超过需要更新的数量则表示是多余的，需要卸载 */
        unmount(oldVnode);
      }
    }

    if (moved) {
      /* 求出source中的最长递增子序列的索引 */
      const seq = getSequence(source);

      /* seq最长递增子序列的索引数组的尾部索引 */
      let s = seq.length - 1;
      /* 新的一组子节点除与旧的一组子节点中首尾相同的子节点外的需要处理的新的子节点的尾部索引 */
      let i = count - 1;

      /* for 循环使 i 递减，从后往前遍历 */
      for (i; i >= 0; i--) {
        if (source[i] === -1) {
          /* 说明是全新的子节点，应该将其挂载 */
          /* 该节点在 newChildren 中的索引 */
          const pos = i + newStartIndex;
          const newVnode = newChildren[pos];
          /* 锚点元素的索引 */
          const nextPos = pos + 1;
          /* 锚点元素 */
          const anchor =
            nextPos < newChildren.length ? newChildren[nextPos].el : null;
          /* 挂载新的元素 */
          patch(null, newVnode, container, anchor);
        } else if (i !== seq[s]) {
          console.log("需要移动子节点对应真实DOM");
          /* 该节点在 newChildren 中的索引 */
          const pos = i + newStartIndex;
          const newVnode = newChildren[pos];
          /* 锚点元素的索引 */
          const nextPos = pos + 1;
          /* 锚点元素 */
          const anchor =
            nextPos < newChildren.length ? newChildren[nextPos].el : null;
          /* 挂载新的元素 */
          insert(newVnode.el, container, anchor);
        } else {
          /* 如果i === seq[s]，表示该节点对应的真实DOM不需要移动 */
          /* 换下一个 */
          s--;
        }
      }
    }
  }
}
